• File: gulpfile.js
  • Full Path: C:/htdocs/javascript/canvas_gauges/gulpfile.js
  • Date Modified: 04/30/2025 7:56 AM
  • File size: 17.13 KB
  • MIME-type: text/plain
  • Charset: utf-8
/**
 * Canv-Gauge Dev Tasks Runner
 *
 * @author Mykhailo Stadnyk <mikhus@gmail.com>
 */
const gulp = require('gulp');
const usage = require('gulp-help-doc');
const browserify = require('browserify');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const rename = require('gulp-rename');
const sourcemaps = require('gulp-sourcemaps');
const uglify = require('gulp-uglify');
const minify = require('gulp-uglify/minifier');
const uglify6 = require('uglify-js-harmony');
const rimraf = require('rimraf');
const esdoc = require('gulp-esdoc');
const eslint = require('gulp-eslint');
const gzip = require('gulp-gzip');
const KarmaServer = require('karma').Server;
const gutil = require('gulp-util');
const chalk = require('chalk');
const http = require('https');
const fs = require('fs');
const selenium = require('selenium-standalone');
const mocha = require('gulp-mocha');
const cp = require('child_process');
const concat = require('gulp-concat');
const yargs = require('yargs');
const replace = require('gulp-replace');
const babel = require('gulp-babel');
const fsc = require('fs-cli');
const semver = require('semver');
const inject = require('gulp-inject-string');
const version = require('./package.json').version;
const plato = require('gulp-plato');

/**
 * @typedef {{argv: object}} yargs
 * @typedef {{parse:function, stringify:function}} JSON
 */

let types = ['all', 'radial', 'linear'];

function es6concat(type = 'all', clean = false) {
    let bundle = [
        'lib/polyfill.js',
        'lib/vendorize.js',
        'lib/EventEmitter.js',
        'lib/Animation.js',
        'lib/DomObserver.js',
        'lib/SmartCanvas.js',
        'lib/GenericOptions.js',
        'lib/Collection.js',
        'lib/BaseGauge.js',
        'lib/drawings.js'
    ];

    if (clean) bundle.shift();

    switch (type.toLowerCase()) {
        case 'radial':
        case 'radial-gauge':
        case 'radialgauge':
            bundle.push('lib/RadialGauge.js');
            break;
        case 'linear':
        case 'linear-gauge':
        case 'lineargauge':
            bundle.push('lib/LinearGauge.js');
            break;
        default:
            bundle.push('lib/RadialGauge.js', 'lib/LinearGauge.js');
    }

    return gulp.src(bundle)
        .pipe(concat('gauge.es6.js'))
        .pipe(replace(/((var|const|let)\s+.*?=\s*)?require\(.*?\);?/g, ''))
        .pipe(replace(/(module\.)?exports(.default)?\s+=\s*.*?\r?\n/g, ''))
        .pipe(replace(/export\s+(default\s+)?(GenericOptions;)?/g, ''))
        .pipe(replace(/%VERSION%/g, version));
}

function wrap(stream) {
    return stream.pipe(replace(/^/, license() + '(function(ns) {'))
        .pipe(replace(/$/,
            ';typeof module !== "undefined" && Object.assign(ns, {' +
            'Collection: Collection,' +
            'GenericOptions: GenericOptions,' +
            'Animation: Animation,' +
            'BaseGauge: BaseGauge,' +
            'drawings: drawings,' +
            'SmartCanvas: SmartCanvas,' +
            'DomObserver: DomObserver,' +
            'vendorize: vendorize' +
            '});' +
            '}(typeof module !== "undefined" ? ' +
            'module.exports : window));'));
}

function es5transpile(type = 'all', withSourceMaps = true, resolve = () => {}) {
    let stream = wrap(es6concat(type)
        .pipe(rename('gauge.es5.js'))
        .pipe(babel({
            presets: ['es2015'],
            compact: false
        }))
        .on('error', function(err) {
            gutil.log(err);
            this.emit('end');
            resolve();
        })
        .pipe(rename('gauge.min.js')));

    if (withSourceMaps) {
        stream = stream.pipe(sourcemaps.init({ loadMaps: true }));
    }

    stream = stream.pipe(uglify({
        preserveComments: (node, comment) => comment.line === 1
    }));

    if (withSourceMaps) {
        stream = stream.pipe(sourcemaps.write('.'));
    }

    return stream;
}

function license() {
    let src = fs.readFileSync('./LICENSE')
        .toString()
        .split(/\r?\n/);

    src.pop();

    return '/*!\n * ' + src.join('\n * ') + '\n *\n * @version ' +
        version + '\n */\n';
}

/**
 * Builds code complexity report
 *
 * @task {complexity}
 */
gulp.task('complexity', done => {
    rimraf('complexity', () => gulp.src('lib/**/*.js')
        .pipe(plato('complexity', {
            complexity: { trycatch: true },
            jshint: {
                options: {
                    strict: false,
                    browser: true,
                    browserify: true,
                    devel: true,
                    expr: true,
                    esversion: 6,
                    laxbreak: true,
                    laxcomma: true,
                    sub: true
                }
            }
        }))
        .on('finish', () => done())
    );
});

/**
 * Displays this usage information.
 *
 * @task {help}
 */
gulp.task('help', () => usage(gulp, { emptyLineBetweenTasks: false }));

/**
 * Builds production packages
 *
 * @task {build:prod}
 * @order {5}
 */
gulp.task('build:prod', done => {
    rimraf('dist', () => {
        Promise.all(types.map(type => {
            return new Promise(resolve => {
                es5transpile(type, false, resolve)
                    .pipe(gulp.dest('dist/' + type))
                    .on('end', () => {
                        let pkg = JSON.parse(fs.readFileSync('./package.json'));

                        delete pkg.devDependencies;
                        delete pkg.scripts;
                        delete pkg.directories;

                        if (type !== 'all') {
                            pkg.version += '-' + type;
                            pkg.keywords.push(type + '-gauge');
                        }

                        else {
                            for (let i = 1; i < types.length; i++) {
                                pkg.keywords.push(types[i] + '-gauge');
                            }
                        }

                        fs.writeFileSync('dist/' + type + '/package.json',
                            JSON.stringify(pkg, null, 2));

                        fs.writeFileSync('dist/' + type + '/README.md',
                            fs.readFileSync('README.md'));

                        fs.writeFileSync('dist/' + type + '/LICENSE',
                            fs.readFileSync('LICENSE'));

                        resolve();
                    });
            });
        })).then(() => {
            let cmd = '';

            console.log(chalk.bold.green('Production packages are now ready!'));
            console.log('To publish each production package, please run the ' +
                'following command:');

            types.reverse().forEach(type => {
                let v = version;
                let entry = type === 'linear' ? './' : '../../';

                if (cmd) cmd += ' && ';

                cmd += 'cd ' + entry + 'dist/' + type;

                if (type === 'all') type = 'latest';
                else v += '-' + type;

                cmd += ' && npm publish';

                if (type !== 'latest')
                    cmd += ' && npm dist-tag add canvas-gauges@' + v + ' ' +
                        type;
            });

            console.log(chalk.grey(cmd));

            let dst = '../canvas-gauges-pages/download';

            fsc.rm(dst + '/' + version);
            fsc.cp('dist', dst + '/' + version, true);
            fsc.rm(dst + '/latest');

            let releases = fsc.ls(dst);
            let latest = semver.maxSatisfying(releases, '*');

            fsc.cp(dst + '/' + latest, dst + '/latest');

            let info = {};

            releases.forEach(release => {
                info[release] = {
                    name: release
                };

                types.forEach(type => {
                    info[release][type] = {
                        bytes: fs.statSync(dst + '/' + release + '/' + type +
                            '/gauge.min.js').size
                    };

                    info[release][type].kb = (info[release][type].bytes / 1024)
                        .toFixed(1);

                    info[release][type].mb = (info[release][type].bytes /
                        (1024 * 1024)).toFixed(1);

                    info[release][type].gb = (info[release][type].bytes /
                        (1024 * 1024 * 1024)).toFixed(1);
                });
            });

            info.latest = JSON.parse(JSON.stringify(info[latest]));

            fs.writeFileSync('../canvas-gauges-pages/_data/releases.json',
                JSON.stringify(info, null, 2));

            done();
        });
    });
});

/**
 * Currently there is no way to minify es6 code, so this task is
 * temporally useless. Awaiting for support of es6 in minifiers.
 *
 * @task {build:es6}
 * @arg {type} build type: 'radial' - Gauge object only, 'linear' - LinearGauge object only, 'all' - everything (default)
 * @order {4}
 */
gulp.task('build:es6', done => {
    es6concat(yargs.argv.type || 'all', true)
        // .pipe(minify({
        //     preserveComments: (node, comment) => comment.line === 1
        // }, uglify6))
        .pipe(gulp.dest('.'))
        .on('end', () => done());
});

/**
 * Clean-ups files from previous build.
 *
 * @task {clean}
 * @order {1}
 */
gulp.task('clean', done => {
    rimraf('gauge.js', () =>
        rimraf('gauge.min.js', () =>
            rimraf('gauge.min.js.map', () =>
                rimraf('gauge.min.js.gz', done))));
});

/**
 * Cleans docs directory
 *
 * @task {clean:docs}
 * @order {6}
 */
gulp.task('clean:docs', done => {
    rimraf('docs', done);
});

/**
 * Builds and minifies es5 code
 *
 * @task {build:es5}
 * @arg {type} build type: 'radial' - Gauge object only, 'linear' - LinearGauge object only, 'all' - everything (default)
 * @order {3}
 */
gulp.task('build:es5', gulp.series('clean', done => {
    es5transpile(yargs.argv.type || 'all')
        .pipe(gulp.dest('.'))
        .on('end', () => {
            if (!process.env.TRVIS) {
                fs.writeFileSync(
                    '../canvas-gauges-pages/' +
                    'assets/js/gauge.min.js',
                    fs.readFileSync('gauge.min.js')
                );
                fs.writeFileSync(
                    '../canvas-gauges-pages/' +
                    'assets/js/gauge.min.js.map',
                    fs.readFileSync('gauge.min.js.map')
                );
            }

            done();
        });
}));

/**
 * Automatically generates API documentation for this project
 * from a source code.
 *
 * @task {doc}
 * @order {7}
 */
gulp.task('doc', gulp.series('clean:docs', done => {
    gulp.src('./lib')
        .pipe(esdoc({
            destination: './docs',
            package: './package.json',
            title: 'HTML5 Canvas Gauges API Documentation',
            styles: [
                './assets/styles/docs.css',
            ]
        }))
        .on('finish', () => {
            if (process.env.TRAVIS) {
                return ;
            }

            console.log(chalk.bold.green('Generating badge...'));

            let coverageDataFile = './docs/coverage.json';
            let coverage = require(coverageDataFile).coverage;
            let color = 'green';

            if (parseFloat(coverage) < 90) {
                color = 'red';
            }

            let url = 'https://img.shields.io/badge/docs-' +
                coverage.replace(/%/g, '%25') +
                '-' + color + '.svg';

            http.get(url, response => {
                if ((response instanceof Error)) {
                    return console.error(response);
                }

                response.pipe(fs.createWriteStream('docs-coverage.svg'));
            });

            //move to pages

            let target = '../canvas-gauges-pages/docs/' + version;

            rimraf(target, () => fs.rename('docs', target, done));
        });
}));

/**
 * Transpiles, combines and minifies JavaScript code.
 *
 * @task {build}
 * @arg {target} compile target, could be 'es5' (default) or 'es6'
 * @arg {type} build type: 'radial' - Gauge object only, 'linear' - LinearGauge object only, 'all' - everything (default)
 * @order {2}
 */
gulp.task('build', gulp.series(
    'build:' + (yargs.argv.target || 'es5'),
    done => done()));

/**
 * Watch for source code changes and automatically re-build
 * when wny of them detected.
 *
 * @task {watch}
 */
gulp.task('watch', () => {
    gulp.watch('lib/**/*.js', gulp.series('build'));
});

/**
 * Runs gzipping for minified file.
 *
 * @task {gzip}
 */
gulp.task('gzip', gulp.series('build:prod', done => {
    Promise.all(types.map(type => new Promise(resolve => {
        gulp.src('dist/' + type + '/gauge.min.js')
            .pipe(gzip({ gzipOptions: { level: 9 } }))
            .pipe(gulp.dest('dist/' + type))
            .on('end', resolve);
    }))).then(() => done());
}));

/**
 * Performs JavaScript syntax check.
 *
 * @task {lint}
 */
gulp.task('lint', () => {
    console.log(chalk.bold.green('\nStarting linting checks...\n'));

    return gulp.src([
            '**/*.js',
            '!node_modules/**',
            '!docs/**',
            '!**.min.js',
            '!coverage/**',
            '!complexity/**',
            '!dist/**',
            '!test/cjs/**'
        ])
        .pipe(eslint())
        .pipe(eslint.format())
        .pipe(eslint.failAfterError());
});

/**
 * Runs unit tests.
 *
 * @task {test:spec}
 */
gulp.task('test:spec', done => {
    console.log(chalk.bold.green('Starting unit tests...'));

    new KarmaServer({
        configFile: __dirname + '/karma.conf.js',
        singleRun: true
    }, exitCode => {
        if (!exitCode) {
            if (process.env.TRAVIS) {
                return done();
            }

            console.log(chalk.bold.green('Generating badge...'));

            // create badges
            let rx = new RegExp(
                '[\u001b\u009b][[()#;?]*' +
                '(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?' +
                '[0-9A-ORZcf-nqry=><]', 'g');
            let coverage = fs.readFileSync(
                    './coverage/report/coverage.txt',
                    { encoding: 'utf8' }
                )
                .replace(rx, '')
                .split(/\r?\n/)[2]
                .split(':')[1]
                .split('(')[0]
                .trim();
            let color = 'green';

            if (parseFloat(coverage) < 90) {
                color = 'red';
            }

            let url = 'https://img.shields.io/badge/coverage-' +
                coverage.replace(/%/g, '%25') +
                '-' + color + '.svg';

            http.get(url, response => {
                if ((response instanceof Error)) {
                    console.error(response);
                    process.exit(0);
                }

                response
                    .pipe(fs.createWriteStream('test-coverage.svg'))
                    .on('finish', () => process.exit(0));
            });

            return done();
        }

        process.exit(exitCode);
    }).start();
});

/**
 * Installs and starts selenium server
 *
 * @task {selenium}
 */
gulp.task('selenium', done => {
    cp.execSync('pkill -f selenium-standalone');

    selenium.install({ logger: gutil.log }, err => {
        if (err) return done(err);

        selenium.start((err, child) => {
            if (err) return done(err);

            selenium.child = child;

            done();
        });
    });
});

/**
 * Runs end-to-end tests.
 *
 * @task {test:e2e}
 *
 * Testing concept could be following:
 *  1. Create various gauge configuration HTML pages
 *  2. Use protractor to navigate this pages
 *  3. On each page, depending on the gauge config and screen res/dpi
 *     checking the proper pixels on canvas if them matching expected color
 *     This could be checks, for example if highlight area in this pixel is
 *     present or not, if arrow starts/ends at this point, etc.
 *     This would be valuable checks, because if something wrong happen with
 *     drawing, most probably expected pixels won't match the expected specs,
 *     so we can automatically figure something is broken.
 */
gulp.task('test:e2e', gulp.series('selenium', done => {
    console.log(chalk.bold.green('\nStarting end-to-end tests...\n'));

    gulp.src(['test/e2e/**/*.e2e.js'])
        .pipe(mocha({ reporter: 'spec' }))
        .once('error', err => {
            console.error(err);
            cp.execSync('pkill -f selenium-standalone');
            process.exit(1);
        })
        .once('end', () => {
            cp.execSync('pkill -f selenium-standalone');
            done();
        });
}));

/**
 * Runs all tests and checks.
 *
 * @task {test}
 */
gulp.task('test', gulp.series('lint', 'test:spec'/*, 'test:e2e'*/));

gulp.task('default', gulp.series('help'));